/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */
package hps.model;

import hps.control.SimulationController;
import java.awt.BasicStroke;
import java.awt.Color;
import java.awt.Graphics;
import java.awt.Graphics2D;
import java.awt.Stroke;
import java.awt.image.BufferedImage;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.Observable;
import java.util.Random;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.imageio.ImageIO;

/**
 *
 * @author $Author: NIKE3500 $
 * @version $Revision: 92 $
 * @date $Date: 2013-01-18 23:26:03 +0000 (Fri, 18 Jan 2013) $
 */
public class Room extends Observable implements IDrawable, Serializable
{

    protected ArrayList<IInfluence> influences;
    protected ArrayList<IInfluence> staticInfluences;
    protected ArrayList<Pedestrian> pedestrians;
    protected IInfluence[][] blockedFields;
    protected int xDim;
    protected int yDim;
    private static Stroke fieldBorderStroke = new BasicStroke(1, BasicStroke.CAP_BUTT, BasicStroke.JOIN_BEVEL, 1, new float[]
            {
                3.0f, 2.0f
            }, 0);

    public Room(int xDim, int yDim)
    {
        this.xDim = xDim;
        this.yDim = yDim;
        blockedFields = new IInfluence[xDim][yDim];
        influences = new ArrayList<IInfluence>();
        pedestrians = new ArrayList<Pedestrian>();
        staticInfluences = new ArrayList<IInfluence>();
    }

    /**
     * Wirft Exception falls Influence nicht mit blockedFields konsistent ist
     * (Ausgehend davon, dass Influence sich bereits im Feld befindet)
     *
     * @param inf
     * @throws IllegalArgumentException
     */
    public void checkConsistancy(IInfluence inf) throws IllegalArgumentException
    {
        for (Point p : inf.getBlockedFields())
        {
            if (!blockedFields[p.getX()][p.getY()].equals(inf))
            {
                throw new IllegalArgumentException(String.format("Karte blockierter Felder ist an (%d/%d) nicht konsistent", p.getX(), p.getY()));
            }
        }
    }

    /**
     * Prueft den Raum auf Konsistenz, schmeißt Exception bei Inkonsistenz
     *
     * @throws IllegalArgumentException wird bei Inkosistent geworfen
     */
    public void checkConsistancy() throws IllegalArgumentException
    {
        //Anlegen eines Prueffeldes
        IInfluence[][] testField = cloneBlockedFields(blockedFields);
        //Einflussseitige Pruefung
        for (Pedestrian p : pedestrians)
        {
            checkConsistancy(p);
            for (Point pnt : p.getBlockedFields())
            {
                testField[pnt.getX()][pnt.getY()] = null;
            }
        }
        for (IInfluence i : staticInfluences)
        {
            checkConsistancy(i);
            for (Point pnt : i.getBlockedFields())
            {
                testField[pnt.getX()][pnt.getY()] = null;
            }
        }
        for (IInfluence i : influences)
        {
            checkConsistancy(i);
            for (Point pnt : i.getBlockedFields())
            {
                testField[pnt.getX()][pnt.getY()] = null;
            }
        }
        //Feldseitige Pruefung
        for (IInfluence[] infs : testField)
        {
            for (IInfluence inf : infs)
            {
                if (inf != null)
                {
                    throw new IllegalArgumentException("blockedFields sind überbelegt");
                }
            }
        }
    }

    public int getxDim()
    {
        return xDim;
    }

    public int getyDim()
    {
        return yDim;
    }

    @Override
    public void draw(Graphics g, Point roomPosition, int singleFieldSize)
    {
        ((Graphics2D) g).setStroke(fieldBorderStroke);
        g.setColor(Color.BLACK);

        //Senkrechte Linien zeichnen
        for (int i = 0; i <= xDim; ++i)
        {
            g.drawLine(roomPosition.getX() + i * singleFieldSize, roomPosition.getY(), roomPosition.getX() + i * singleFieldSize, roomPosition.getY() + yDim * singleFieldSize);
        }
        //Waagerechte Linien zeichnen
        for (int i = 0; i <= yDim; ++i)
        {
            g.drawLine(roomPosition.getX(), roomPosition.getY() + i * singleFieldSize, roomPosition.getX() + xDim * singleFieldSize, roomPosition.getY() + i * singleFieldSize);
        }

        //Influences zeichen
        for (IInfluence i : staticInfluences)
        {
            i.draw(g, roomPosition, singleFieldSize);
        }
        for (Pedestrian p : pedestrians)
        {
            p.draw(g, roomPosition, singleFieldSize);
        }
        for (IInfluence i : influences)
        {
            i.draw(g, roomPosition, singleFieldSize);
        }
    }

    /**
     * Erstellt einen neuen Simulationscontroller fuer den Raum
     *
     * @return Simulationscontroller
     */
    public SimulationController createSimulation()
    {
        SimulationController rtrn = new SimulationController(blockedFields, pedestrians, influences, staticInfluences);

        return rtrn;
    }    

    /**
     * Fuegt neuen Fussgaenger hinzu Benachrichtigt ueber Aenderungen.
     *
     * @param newPed Hnzuzufuegender Fussgaenger
     * @throws IllegalArgumentException Falls Feld besetzt oder ausserhalb des
     * Feldes ist
     */
    public void addPedestrian(Pedestrian newPed) throws IllegalArgumentException
    {
        addPedestrianSilent(newPed);
        notifyChanges();
    }

    /**
     * Fuegt neuen Fussgaenger hinzu
     *
     * @param newPed Hnzuzufuegender Fussgaenger
     * @throws IllegalArgumentException Falls Feld besetzt oder ausserhalb des
     * Feldes ist
     */
    protected void addPedestrianSilent(Pedestrian newPed) throws IllegalArgumentException
    {
        //Exception falls Einfluss nicht zulässig
        checkAddableInfluence(newPed);
        //Sonst hinzufuegen
        pedestrians.add(newPed);
        for (Point p : newPed.getBlockedFields())
        {
            blockedFields[p.getX()][p.getY()] = newPed;
        }
    }

    /**
     * Fuellt einen rechteckigen Bereich zufaellig mit Pedestrians
     *
     * @param p1 Obere linke Ecke der Bereichs
     * @param p2 Untere rechte Ecke des Bereichs
     * @param count Anzahl der Pedestrians
     * @return Anzahl der hinzugefuegten Pedestrians
     */
    public int addRandomPedestrians(Point p1, Point p2, int count)
    {
        ArrayList<Point> l_Points = new ArrayList<Point>();
        Random rand = new Random();
        Point currentPoint;
        int rtrn = 0;
        //Liste mit noetigen Punkten aufbauen
        for (int i = p1.getX(); i <= p2.getX(); ++i)
        {
            for (int j = p1.getY(); j <= p2.getY(); ++j)
            {
                l_Points.add(new Point(i, j));
            }
        }
        //Zufaellig Punkte aussuchen und Versuchen zu belegen
        while ((count > 0) && (l_Points.size() > 0))
        {
            currentPoint = l_Points.get(rand.nextInt(l_Points.size()));
            l_Points.remove(currentPoint);
            if (blockedFields[currentPoint.getX()][currentPoint.getY()] == null)
            {
                addPedestrianSilent(new Pedestrian(currentPoint.getX(), currentPoint.getY()));
                ++rtrn;
                --count;
            }
        }
        //Benachrichtigung und Rueckgabe
        notifyChanges();
        return rtrn;
    }

    /**
     * Fuegt neuen Einfluss hinzu. Benachrichtigt ueber Aenderungen
     *
     * @param newInf neuer Einfluss
     */
    public void addInfluence(IInfluence newInf) throws IllegalArgumentException
    {
        addInfluenceSilent(newInf);
        notifyChanges();
    }

    /**
     * Fuegt neuen Einfluss hinzu
     *
     * @param newInf neuer Einfluss
     */
    protected void addInfluenceSilent(IInfluence newInf) throws IllegalArgumentException
    {
        if (!influences.contains(newInf))
        {
            //Exception falls Einfluss nicht zulässig
            checkAddableInfluence(newInf);
            //Sonst hinzufuegen
            influences.add(newInf);
            for (Point p : newInf.getBlockedFields())
            {
                blockedFields[p.getX()][p.getY()] = newInf;
            }
        }
    }

    /**
     * Fuegt neuen statischen Einfluss hinzu (erzeugt einmalig Wirkung, breitet
     * sich nicht aus!) Benachrichtigt ueber Aenderungen
     *
     * @param newInf neuer Einfluss
     */
    public void addStaticInfluence(IInfluence newInf) throws IllegalArgumentException
    {
        addStaticInfluenceSilent(newInf);
        notifyChanges();
    }

    /**
     * Fuegt neuen statischen Einfluss hinzu (erzeugt einmalig Wirkung, breitet
     * sich nicht aus!)
     *
     * @param newInf
     */
    protected void addStaticInfluenceSilent(IInfluence newInf)
    {
        //Exception falls Einfluss nicht zulässig
        checkAddableInfluence(newInf);
        //Sonst hinzufuegen
        staticInfluences.add(newInf);
        for (Point p : newInf.getBlockedFields())
        {
            blockedFields[p.getX()][p.getY()] = newInf;
        }
    }

    /**
     * Prueft ob Einfluss hinzufuegbar ist
     *
     * @param newInf Zu prüfender Einfluss
     * @throws IllegalArgumentException Falls Einfluss nicht hinzufuegbar ist
     */
    private void checkAddableInfluence(IInfluence newInf) throws IllegalArgumentException
    {
        if (newInf.isBlocking())
        {
            for (Point p : newInf.getBlockedFields())
            {
                if ((p.getX() >= xDim) || (p.getY() >= yDim))
                {
                    throw new IllegalArgumentException("Position ist nicht im Kartenbereich.");
                }
                if (blockedFields[p.getX()][p.getY()] != null)
                {
                    throw new IllegalArgumentException(String.format("Position (%d/%d) ist bereits besetzt", p.getX(), p.getY()));
                }
            }
        }
    }

    /**
     * Entfernt einen EInfluss, falls vorhanden. Räumt blockedFields mit auf.
     * Benachrichtigt ueber Aenderungen.
     *
     * @param inf zu entfernender Einfluss
     */
    public void removeInfluence(IInfluence inf)
    {
        removeInfluenceSilent(inf);
        notifyChanges();
    }

    /**
     * Entfernt einen EInfluss, falls vorhanden. Räumt blockedFields mit auf
     *
     * @param inf zu entfernender Einfluss
     */
    protected void removeInfluenceSilent(IInfluence inf)
    {
        if (influences.contains(inf))
        {
            //Entfernen aus Liste
            influences.remove(inf);
            //Aufräumen der Map
            for (Point p : inf.getBlockedFields())
            {
                blockedFields[p.getX()][p.getY()] = null;
            }
        }
    }

    /**
     * Entfernt einen statischen Einfluss, falls vorhanden. Räumt blockedFields
     * mit auf. Benachrichtigt ueber Aenderungen.
     *
     * @param inf zu entfernender Einfluss
     */
    public void removeStaticInfluence(IInfluence inf)
    {
        removeStaticInfluenceSilent(inf);
        notifyChanges();
    }

    /**
     * Entfernt einen statischen Einfluss, falls vorhanden. Räumt blockedFields
     * mit auf.
     *
     * @param inf zu entfernender Einfluss
     */
    protected void removeStaticInfluenceSilent(IInfluence inf)
    {
        if (staticInfluences.contains(inf))
        {
            //Entfernen aus Liste
            staticInfluences.remove(inf);
            //Aufräumen der Map
            for (Point p : inf.getBlockedFields())
            {
                blockedFields[p.getX()][p.getY()] = null;
            }
        }
    }

    /**
     * Entfernt alle Einfluesse an einer bestimmten Position
     *
     * @param p
     */
    public void removeAt(Point p)
    {
        removeAtSilent(p);
        notifyChanges();
    }

    /**
     * Entfernt alle Einfluesse an einer bestimmten Position
     *
     * @param p
     */
    protected void removeAtSilent(Point p)
    {
        //dynamsiche Einfluesse bereinigen
        for (int i = 0; i < influences.size(); i++)
        {
            if (influences.get(i).isAt(p))
            {
                removeInfluenceSilent(influences.get(i--));
            }
        }
        //statische Einfluesse bereinigen
        for (int i = 0; i < staticInfluences.size(); i++)
        {
            if (staticInfluences.get(i).isAt(p))
            {
                removeStaticInfluenceSilent(staticInfluences.get(i--));
            }
        }
        //Pedestrians bereinigen
        for (int i = 0; i < pedestrians.size(); i++)
        {
            if (pedestrians.get(i).isAt(p))
            {
                removePedestrianSilent(pedestrians.get(i--));
            }
        }
    }

    /**
     * Entfernt einen Pedestrian, falls vorhanden. Räumt blockedFields mit auf.
     *
     * @param ped zu entfernender Pedestrian
     */
    public void removePedestrian(Pedestrian ped)
    {
        removePedestrianSilent(ped);
        notifyChanges();
    }

    /**
     * Entfernt einen Pedestrian, falls vorhanden. Räumt blockedFields mit auf.
     *
     * @param ped zu entfernender Pedestrian
     */
    protected void removePedestrianSilent(Pedestrian ped)
    {
        if (pedestrians.contains(ped))
        {
            //Entfernen aus Liste
            pedestrians.remove(ped);
            //Aufräumen der Map
            for (Point p : ped.getBlockedFields())
            {
                blockedFields[p.getX()][p.getY()] = null;
            }
        }
    }

    /**
     * Serialisiert Raum in eine Datei, um ihn später erneut laden zu können
     * 
     * @param path zu speichernde Datei als Pfad
     * @throws FileNotFoundException wenn die Datei nicht gefunden oder
     * geoeffnet werden kann
     * @throws IOException
     */
    public void saveTo(String path) throws  IOException
    {
        FileOutputStream f = new FileOutputStream(path);
        ObjectOutputStream os = new ObjectOutputStream(f);
        os.writeObject(this);
        os.close();
        f.close();
    }
    
    /**
     * Speicher Bild des Raum als png
     *
     * @param path zu speichernde Datei als Pfad
     * @throws FileNotFoundException wenn die Datei nicht gefunden oder
     * geoeffnet werden kann
     * @throws IOException
     */
    public void saveImageTo(String path) throws  IOException
    {
        BufferedImage img = new BufferedImage(xDim*48, yDim*48, BufferedImage.TYPE_INT_ARGB);
        Graphics g = img.getGraphics();
        g.setColor(Color.WHITE);
        g.fillRect(0, 0, xDim*48, yDim*48);
        this.draw(g, new Point(0, 0), 48);
        ImageIO.write(img, "png", new File(path));
    }

    /**
     *
     * @param path zu lesende Datei als Pfad
     * @return den gelesenen Raum
     * @throws FileNotFoundException
     * @throws IOException
     */
    public static Room readFrom(String path) throws FileNotFoundException, IOException
    {
        FileInputStream f = new FileInputStream(path);
        ObjectInputStream is = new ObjectInputStream(f);
        Room r = null;
        try
        {
            r = (Room) is.readObject();
        } catch (ClassNotFoundException ex)
        {
            Logger.getLogger(Room.class.getName()).log(Level.SEVERE, null, ex);
        } finally
        {
            is.close();
            f.close();
        }
        return r;
    }

    public void notifyChanges()
    {
        setChanged();
        notifyObservers();
        clearChanged();
    }

    @Override
    public boolean equals(Object o)
    {
        if (o == null)
        {
            return false;
        }
        if (!(o instanceof Room))
        {
            return false;
        }
        if (!(o.hashCode() == this.hashCode()))
        {
            return false;
        }
        return true;
    }

    @Override
    public int hashCode()
    {
        int hash = 5;
        hash = 13 * hash + (this.pedestrians != null ? this.pedestrians.hashCode() : 0);
        hash = 13 * hash + this.xDim;
        hash = 13 * hash + this.yDim;
        return hash;
    }
    
    /**
     * Klont ein IInfluence[][]-Array
     * @param blockedFields
     * @return 
     */
    protected final static IInfluence[][] cloneBlockedFields(IInfluence[][] blockedFields)
    {
        IInfluence[][] rtrn = new IInfluence[blockedFields.length][blockedFields[0].length];
        for (int i = 0; i < blockedFields.length; i++)
        {
            System.arraycopy(blockedFields[i], 0, rtrn[i], 0, blockedFields[0].length);
        }
        return rtrn;
    }
}
